package org.rails.core.sql;

/*
 *  Copyright 2008-2010 www.jrails.org
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.rails.core.db.table.NameUtils;
import org.rails.core.db.table.Table;
import org.rails.core.model.ActiveRecord;
import org.rails.core.resource.ConfigBuilder;
import org.rails.core.resource.ResourceFactory;
import org.rails.core.util.Utils;

public final class Relation {

	private final static ConfigBuilder config = ResourceFactory.getConfigBuilder();
	private final static String COLUMN = "column";
	private final static String JOIN = "join";
	private final static String CONDITION = "condition";
	private final static String ORDER = "order";
	private final static String LAST = "last";
	private final static String HAVING = "having";
	private final static String GROUP = "group";
	private final static String FOREIGN_KEY = "foreignKey";
	private final static String BELONGS_TO = "belongsTo";
	private final static String HAS_ONE = "hasOne";
	private final static String HAS_MANY = "hasMany";
	private final static String MARRIES = "marries";

	private Table srcTable;
	private Table tarTable;
	private SQLManager sqlManager;
	private ActiveRecord source;
	private ActiveRecord target;
	private Map<String, Object> params ;

	private String asName;
	
	public Relation(ActiveRecord source, ActiveRecord target) {
		this.source = source;
		this.target = target;
		params = new HashMap<String,Object>();
		srcTable = source.getTable();
		tarTable = target.getTable();
		this.asName = target.getClass().getSimpleName();
	}

	public Relation(ActiveRecord source, ActiveRecord target, String asName) {
		this.source = source;
		this.target = target;
		params = new HashMap<String,Object>();
		srcTable = source.getTable();
		tarTable = target.getTable();
		this.asName = asName;
	}

	public Relation(ActiveRecord source, ActiveRecord target,
			Map<String, Object> params) {
		this.source = source;
		this.target = target;
		this.params = params;
		srcTable = source.getTable();
		tarTable = target.getTable();
		this.asName = target.getClass().getSimpleName();
	}

	public Relation(ActiveRecord current, ActiveRecord target, String asName,
			Map<String, Object> params) {
		this.source = current;
		this.target = target;
		this.params = params;
		srcTable = current.getTable();
		tarTable = target.getTable();
		this.asName = asName;
	}

	public SQLManager getBelongsToSqlManager() {
		final String fk = getForeignKey(BELONGS_TO, tarTable);
		sqlManager = new SQLManager(target);
		sqlManager.addCondition(Utils.format("{0}.{1} = :{2}", tarTable
				.getFormatName(), tarTable.getFormatPrimaryKey(), fk));
		sqlManager.addParameter(fk, source.get(fk));		
		sqlManager.addColumn(getColumn(BELONGS_TO,Utils.format("{0}.*", tarTable.getFormatName())));
		sqlManager.addCondition(getCondition(BELONGS_TO));
		sqlManager.addJoin(getJoin(BELONGS_TO));
		sqlManager.addOrder(getOrder(BELONGS_TO));
		sqlManager.addGroup(getGroup(BELONGS_TO));
		sqlManager.addHaving(getHaving(BELONGS_TO));
		sqlManager.addLast(getLast(BELONGS_TO));
		sqlManager.addParameter(params);
		return sqlManager;
	}

	public SQLManager getHasOneSqlManager() {
		final String fk = getForeignKey(HAS_ONE, srcTable);
		sqlManager = new SQLManager(target);
		sqlManager.addCondition(Utils.format("{0}.{1} = :{2}", tarTable
				.getFormatName(),tarTable.formatColumn(fk), fk));
		sqlManager.addParameter(fk, source.get(srcTable.getPrimaryKey()));		
		sqlManager.addColumn(getColumn(HAS_ONE,Utils.format("{0}.*", tarTable.getFormatName())));
		sqlManager.addCondition(getCondition(HAS_ONE));
		sqlManager.addJoin(getJoin(HAS_ONE));
		sqlManager.addOrder(getOrder(HAS_ONE));
		sqlManager.addGroup(getGroup(HAS_ONE));
		sqlManager.addHaving(getHaving(HAS_ONE));
		sqlManager.addLast(getLast(HAS_ONE));
		sqlManager.addParameter(params);
		return sqlManager;
	}

	public SQLManager getHasManySqlManager() {
		final String fk = getForeignKey(HAS_MANY, srcTable);
		sqlManager = new SQLManager(target);
		sqlManager.addCondition(Utils.format("{0}.{1} = :{2}", tarTable
				.getFormatName(),tarTable.formatColumn(fk), fk));
		sqlManager.addParameter(fk, source.get(srcTable.getPrimaryKey()));
		sqlManager.addColumn(getColumn(HAS_MANY,Utils.format("{0}.*", tarTable.getFormatName())));
		sqlManager.addCondition(getCondition(HAS_MANY));
		sqlManager.addJoin(getJoin(HAS_MANY));
		sqlManager.addOrder(getOrder(HAS_MANY));
		sqlManager.addGroup(getGroup(HAS_MANY));
		sqlManager.addHaving(getHaving(HAS_MANY));
		sqlManager.addLast(getLast(HAS_MANY));
		sqlManager.addParameter(params);
		return sqlManager;
	}

	public SQLManager getMarriesSqlManager() {
		final String relaTabName = createRelationTableName();
		sqlManager = new SQLManager(target);
		final String join = "INNER JOIN {0} ON {0}.{1} = {2}.{3}";
		final String currFk = Utils.format("{0}_{1}", srcTable.getName(), srcTable.getPrimaryKey());
		final String targetFk = Utils.format("{0}_{1}", tarTable.getName(), tarTable.getPrimaryKey());
		
		final String[] param = new String[] { relaTabName, targetFk,
				tarTable.getFormatName(), tarTable.getFormatPrimaryKey() };
		sqlManager.addJoin(Utils.format(join, param));		
		sqlManager.addCondition(Utils.format("{0}.{1} = :{2}",relaTabName,currFk,currFk));
		sqlManager.addParameter(currFk, source.getId());
		sqlManager.addColumn(getColumn(MARRIES,Utils.format("{0}.*", tarTable.getFormatName())));
		sqlManager.addCondition(getCondition(MARRIES));
		sqlManager.addJoin(getJoin(MARRIES));
		sqlManager.addOrder(getOrder(MARRIES));
		sqlManager.addGroup(getGroup(MARRIES));
		sqlManager.addHaving(getHaving(MARRIES));
		sqlManager.addLast(getLast(MARRIES));
		sqlManager.addParameter(params);
		return sqlManager;
	}
	
	public String getMarriedSql() {
		final String relaTabName = createRelationTableName();
		final String sql = "SELECT COUNT(*) AS c FROM {0} WHERE {1} = ? AND {2} = ?";
		final String currFk = marryFormatCol(Utils.format("{0}_id",srcTable.getName()));
		final String targetFk = marryFormatCol(Utils.format("{0}_id", tarTable.getName()));
		return Utils.format(sql, relaTabName, currFk, targetFk);
	}

	public String getUnMarrySql() {
		final String relaTabName = createRelationTableName();
		final String sql = "DELETE FROM {0} WHERE {1} = ? AND {2} = ?";
		final String currFk = marryFormatCol(Utils.format("{0}_id", srcTable.getName()));
		final String targetFk = marryFormatCol(Utils.format("{0}_id", tarTable.getName()));
		return Utils.format(sql, relaTabName, currFk, targetFk);
	}
	
	public String getMarrySql(){
		final String relaTabName = createRelationTableName();
		final String sql = "INSERT INTO {0}({1},{2}) VALUES(?,?)";
		final String currFk = marryFormatCol(Utils.format("{0}_id", srcTable.getName()));
		final String targetFk = marryFormatCol(Utils.format("{0}_id", tarTable.getName()));
		return Utils.format(sql, relaTabName, currFk, targetFk);
	}

	private String createRelationTableName() {
		String rtab = createRelationTable();
		String pasName = NameUtils.toPascalName(rtab);
		String pfx = config.getConfigValue(Utils.format("{0}.table.prefix",pasName),"");
		String frm = config.getConfigValue(Utils.format("{0}.table.format",pasName),"[table]");	
		return frm.replaceFirst("\\[table\\]",pfx + rtab);
	}
	
	private String marryFormatCol(String column){
		String rtab = createRelationTable();
		String pasName = NameUtils.toPascalName(rtab);
		String frm = config.getConfigValue(Utils.format("{0}.column.format",pasName),"[column]");
		return frm.replaceFirst("\\[column\\]", column);
	}
	
	private String createRelationTable(){
		final List<String> relLs = new ArrayList<String>();
		relLs.add(srcTable.getName());
		relLs.add(tarTable.getName());
		Collections.sort(relLs);
		return Utils.format("{0}_{1}", relLs.get(0), relLs.get(1));
	}

	private String getColumn(String relation,String def) {
		return get(relation, Relation.COLUMN,def);
	}

	private String getJoin(String relation) {
		return get(relation, Relation.JOIN,"");
	}

	private String getCondition(String relation) {
		return get(relation, Relation.CONDITION,"");
	}

	private String getOrder(String relation) {
		return get(relation, Relation.ORDER,"");
	}
	
	private String getGroup(String relation) {
		return get(relation, Relation.GROUP,"");
	}

	private String getLast(String relation) {
		return get(relation, Relation.LAST,"");
	}

	private String getHaving(String relation) {
		return get(relation, Relation.HAVING,"");
	}

	private String getForeignKey(String relation, Table t) {
		return get(relation, Relation.FOREIGN_KEY,
							Utils.format("{0}_{1}",t.getName(), t.getPrimaryKey()));
	}

	private String get(String relation, String type,String def) {
		final String key = Utils.format("{0}.{1}.{2}.{3}",
				source.getClass().getSimpleName(), relation, asName, type);
		return config.getConfigValue(key,def);
	}

}
